package cn.ctodb.web.rest;

import com.codahale.metrics.annotation.Timed;

import cn.ctodb.domain.PersistentToken;
import cn.ctodb.domain.User;
import cn.ctodb.repository.PersistentTokenRepository;
import cn.ctodb.repository.UserRepository;
import cn.ctodb.security.SecurityUtils;
import cn.ctodb.service.SysMailService;
import cn.ctodb.service.UserService;
import cn.ctodb.web.rest.dto.KeyAndPasswordDTO;
import cn.ctodb.web.rest.dto.ManagedUserDTO;
import cn.ctodb.web.rest.dto.UserDTO;
import cn.ctodb.web.rest.util.HeaderUtil;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;

/**
 * REST controller for managing the current user's account.
 */
@RestController
@RequestMapping("/api")
public class AccountResource {

    private final Logger              log = LoggerFactory.getLogger(AccountResource.class);

    @Inject
    private UserRepository            userRepository;

    @Inject
    private UserService               userService;

    @Inject
    private PersistentTokenRepository persistentTokenRepository;

    @Inject
    private SysMailService               mailService;

    /**
     * POST /register : register the user.
     *
     * @param managedUserDTO
     *            the managed user DTO
     * @param request
     *            the HTTP request
     * @return the ResponseEntity with status 201 (Created) if the user is
     *         registred or 400 (Bad Request) if the login or e-mail is already
     *         in use
     */
    @RequestMapping(value = "/register", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE })
    @Timed
    public ResponseEntity<?> registerAccount(@Valid @RequestBody ManagedUserDTO managedUserDTO, HttpServletRequest request) {

        HttpHeaders textPlainHeaders = new HttpHeaders();
        textPlainHeaders.setContentType(MediaType.TEXT_PLAIN);

        return userRepository.findOneByCode(managedUserDTO.getCode().toLowerCase())
                .map(user -> new ResponseEntity<>("login already in use", textPlainHeaders, HttpStatus.BAD_REQUEST))
                .orElseGet(() -> userRepository.findOneByEmail(managedUserDTO.getEmail())
                        .map(user -> new ResponseEntity<>("e-mail address already in use", textPlainHeaders, HttpStatus.BAD_REQUEST))
                        .orElseGet(() -> {
                            User user = userService.createUserInformation(managedUserDTO.getCode(), managedUserDTO.getPassword(),
                                    managedUserDTO.getName(), managedUserDTO.getEmail().toLowerCase(), managedUserDTO.getLangKey());
                            String baseUrl = request.getScheme() + // "http"
                            "://" + // "://"
                            request.getServerName() + // "myhost"
                            ":" + // ":"
                            request.getServerPort() + // "80"
                            request.getContextPath(); // "/myContextPath" or ""
                                                      // if deployed in root
                                                      // context

                            mailService.sendActivationEmail(user, baseUrl);
                            return new ResponseEntity<>(HttpStatus.CREATED);
                        }));
    }

    /**
     * GET /activate : activate the registered user.
     *
     * @param key
     *            the activation key
     * @return the ResponseEntity with status 200 (OK) and the activated user in
     *         body, or status 500 (Internal Server Error) if the user couldn't
     *         be activated
     */
    @RequestMapping(value = "/activate", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    public ResponseEntity<String> activateAccount(@RequestParam(value = "key") String key) {
        return userService.activateRegistration(key).map(user -> new ResponseEntity<String>(HttpStatus.OK))
                .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    /**
     * GET /authenticate : check if the user is authenticated, and return its
     * login.
     *
     * @param request
     *            the HTTP request
     * @return the login if the user is authenticated
     */
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    public String isAuthenticated(HttpServletRequest request) {
        log.debug("REST request to check if the current user is authenticated");
        return request.getRemoteUser();
    }

    /**
     * GET /account : get the current user.
     *
     * @return the ResponseEntity with status 200 (OK) and the current user in
     *         body, or status 500 (Internal Server Error) if the user couldn't
     *         be returned
     */
    @RequestMapping(value = "/account", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    public ResponseEntity<UserDTO> getAccount() {
        return Optional.ofNullable(userService.getUserWithAuthorities()).map(user -> new ResponseEntity<>(new UserDTO(user), HttpStatus.OK))
                .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    /**
     * POST /account : update the current user information.
     *
     * @param userDTO
     *            the current user information
     * @return the ResponseEntity with status 200 (OK), or status 400 (Bad
     *         Request) or 500 (Internal Server Error) if the user couldn't be
     *         updated
     */
    @RequestMapping(value = "/account", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    public ResponseEntity<String> saveAccount(@Valid @RequestBody UserDTO userDTO) {
        Optional<User> existingUser = userRepository.findOneByEmail(userDTO.getEmail());
        if (existingUser.isPresent() && (!existingUser.get().getCode().equalsIgnoreCase(userDTO.getCode()))) {
            return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert("user-management", "emailexists", "Email already in use"))
                    .body(null);
        }
        return userRepository.findOneByCode(SecurityUtils.getCurrentUserCode()).map(u -> {
            userService.updateUserInformation(userDTO.getName(), userDTO.getEmail(), userDTO.getLangKey());
            return new ResponseEntity<String>(HttpStatus.OK);
        }).orElseGet(() -> new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    /**
     * POST /account/change_password : changes the current user's password
     *
     * @param newpwd
     *            the new password
     * @return the ResponseEntity with status 200 (OK), or status 400 (Bad
     *         Request) if the new password is not strong enough
     */
    @RequestMapping(value = "/account/change_password", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
    @Timed
    public ResponseEntity<?> changePassword(@RequestParam(value = "newpwd") String newpwd, @RequestParam(value = "srcpwd") String srcpwd) {
        if (!checkPasswordLength(newpwd)) {
            return new ResponseEntity<>("Incorrect password", HttpStatus.BAD_REQUEST);
        }
        userService.changePassword(srcpwd, newpwd);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    /**
     * GET /account/sessions : get the current open sessions.
     *
     * @return the ResponseEntity with status 200 (OK) and the current open
     *         sessions in body, or status 500 (Internal Server Error) if the
     *         current open sessions couldn't be retrieved
     */
    @RequestMapping(value = "/account/sessions", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    public ResponseEntity<List<PersistentToken>> getCurrentSessions() {
        return userRepository.findOneByCode(SecurityUtils.getCurrentUserCode())
                .map(user -> new ResponseEntity<>(persistentTokenRepository.findByUser(user), HttpStatus.OK))
                .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    /**
     * DELETE /account/sessions?series={series} : invalidate an existing
     * session.
     *
     * - You can only delete your own sessions, not any other user's session -
     * If you delete one of your existing sessions, and that you are currently
     * logged in on that session, you will still be able to use that session,
     * until you quit your browser: it does not work in real time (there is no
     * API for that), it only removes the "remember me" cookie - This is also
     * true if you invalidate your current session: you will still be able to
     * use it until you close your browser or that the session times out. But
     * automatic login (the "remember me" cookie) will not work anymore. There
     * is an API to invalidate the current session, but there is no API to check
     * which session uses which cookie.
     *
     * @param series
     *            the series of an existing session
     * @throws UnsupportedEncodingException
     *             if the series couldnt be URL decoded
     */
    @RequestMapping(value = "/account/sessions/{series}", method = RequestMethod.DELETE)
    @Timed
    public void invalidateSession(@PathVariable String series) throws UnsupportedEncodingException {
        String decodedSeries = URLDecoder.decode(series, "UTF-8");
        userRepository.findOneByCode(SecurityUtils.getCurrentUserCode()).ifPresent(u -> {
            persistentTokenRepository.findByUser(u).stream().filter(persistentToken -> StringUtils.equals(persistentToken.getSeries(), decodedSeries))
                    .findAny().ifPresent(t -> persistentTokenRepository.delete(decodedSeries));
        });
    }

    /**
     * POST /account/reset_password/init : Send an e-mail to reset the password
     * of the user
     *
     * @param mail
     *            the mail of the user
     * @param request
     *            the HTTP request
     * @return the ResponseEntity with status 200 (OK) if the e-mail was sent,
     *         or status 400 (Bad Request) if the e-mail address is not
     *         registred
     */
    @RequestMapping(value = "/account/reset_password/init", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
    @Timed
    public ResponseEntity<?> requestPasswordReset(@RequestBody String mail, HttpServletRequest request) {
        return userService.requestPasswordReset(mail).map(user -> {
            String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
            mailService.sendPasswordResetMail(user, baseUrl);
            return new ResponseEntity<>("e-mail was sent", HttpStatus.OK);
        }).orElse(new ResponseEntity<>("e-mail address not registered", HttpStatus.BAD_REQUEST));
    }

    /**
     * POST /account/reset_password/finish : Finish to reset the password of the
     * user
     *
     * @param keyAndPassword
     *            the generated key and the new password
     * @return the ResponseEntity with status 200 (OK) if the password has been
     *         reset, or status 400 (Bad Request) or 500 (Internal Server Error)
     *         if the password could not be reset
     */
    @RequestMapping(value = "/account/reset_password/finish", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
    @Timed
    public ResponseEntity<String> finishPasswordReset(@RequestBody KeyAndPasswordDTO keyAndPassword) {
        if (!checkPasswordLength(keyAndPassword.getNewPassword())) {
            return new ResponseEntity<>("Incorrect password", HttpStatus.BAD_REQUEST);
        }
        return userService.completePasswordReset(keyAndPassword.getNewPassword(), keyAndPassword.getKey())
                .map(user -> new ResponseEntity<String>(HttpStatus.OK)).orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    private boolean checkPasswordLength(String password) {
        return (!StringUtils.isEmpty(password) && password.length() >= ManagedUserDTO.PASSWORD_MIN_LENGTH
                && password.length() <= ManagedUserDTO.PASSWORD_MAX_LENGTH);
    }
}
